import * as crypto from 'crypto';

let intformat = require('biguint-format');
let FlakeId = require('flake-idgen');
let flakeIdGen = new FlakeId({epoch: 1300000000000});
import * as urlencode from 'urlencode';
import * as os from 'os';
import * as util from 'util';
import * as buffer from "buffer";

export enum SIGN_TYPE {
    SIGN_TYPE_HMACSHA256 = 'HMAC-SHA256',
    SIGN_TYPE_MD5 = 'MD5'
}

export enum SIGN_KEY_TYPE {
    KEY = 'key',
    SECRET = 'secret'
}

export class Utils {
    public static FIELD_SIGN = 'sign';
    public static FIELD_SIGN_TYPE = 'sign_type';

    constructor() {
    }

    /**
     * md5 加密
     * @param data
     */
    public static md5(data: string): string {
        return this.hash(data, 'md5')
    }

    /**
     *
     * @param {string} data
     * @param {string} algorithm
     * @returns {string}
     */
    public static hash(data: string, algorithm: string) {
        return crypto
            .createHash(algorithm)
            .update(data, 'utf8')
            .digest('hex')
    }

    /**
     *
     * @param {module:crypto.BinaryLike} data
     * @param {string} algorithm
     * @returns {string}
     */
    public static hashx(data: crypto.BinaryLike, algorithm: string) {
        return crypto
            .createHash(algorithm)
            .update(data)
            .digest('hex')
    }

    /**
     * hmacsha256 加密
     * @param data
     * @param key
     */
    public static hmacsha256(data: string, key: string): string {
        return crypto
            .createHmac('sha256', key)
            .update(data, 'utf8')
            .digest('hex')
    }

    /**
     * SHA256withRSA 验证签名
     * @param publicKey 公钥key
     * @param signature 待验证的签名串
     * @param data 需要验证的字符串
     */
    public static sha256WithRsaVerify(publicKey: string, signature: string, data: string) {
        console.log(data);
        return crypto
            .createVerify('RSA-SHA256')
            .update(data)
            .verify(publicKey, signature, 'base64')
    }


    /**
     * 验证签名
     * @param headers     http 请求头
     * @param body        应答主体
     * @param publicKey   平台公钥
     */
    public static verifySign(headers: any, body: string, publicKey: string): boolean {
        let timestamp = headers['wechatpay-timestamp'];
        let nonce = headers['wechatpay-nonce'];
        let signature = headers['wechatpay-signature'];
        let buildSignMessage: string = this.buildRepSignMessage(timestamp, nonce, body);
        return this.sha256WithRsaVerify(publicKey, signature, buildSignMessage)
    }

    /**
     * 构建应答签名参数
     * @param timestamp 应答时间戳
     * @param nonceStr 应答随机串
     * @param body 应答报文主体
     */
    public static buildRepSignMessage(timestamp: string, nonceStr: string, body: string): string {
        return this.buildSignMessage([timestamp, nonceStr, body])
    }

    /**
     * 构建签名参数
     * @param {Array<string>} data 待构建签名的参数
     * @returns {string}           返回待签名字符串
     */
    public static buildSignMessage(data: Array<string>): any {
        if (!data || data.length <= 0) {
            return null
        }
        let sign: string = '';
        data.forEach(item => {
            sign = sign.concat(item).concat('\n')
        });
        return sign
    }

    /**
     * AEAD_AES_256_GCM 解密
     * @param key  apiv3 key
     * @param nonce
     * @param associatedData
     * @param ciphertext
     */
    public static aes256gcmDecrypt(key: string, nonce: string, associatedData: string, ciphertext: string): string {
        let ciphertextBuffer = Buffer.from(ciphertext, 'base64');
        let authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16);
        let data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16);
        let decipherIv = crypto.createDecipheriv('aes-256-gcm', key, nonce);
        decipherIv.setAuthTag(Buffer.from(authTag));
        decipherIv.setAAD(Buffer.from(associatedData));
        let decryptStr = decipherIv.update(data, undefined, 'utf8');
        decipherIv.final();
        return decryptStr
    }

    /**
     * SHA256withRSA
     * @param data 待加密字符
     * @param privatekey 私钥key  key.pem
     */
    public static sha256WithRsa(privatekey: Buffer, data: string): string {
        return crypto.createSign('RSA-SHA256').update(data).sign(privatekey, 'base64')
    }

    /**
     * 生成签名
     * @param data
     * @param key
     * @param signTypeParam 签名类型
     */
    public static generateSignature(data: any, key: string, signTypeParam: SIGN_TYPE, signKeyType = SIGN_KEY_TYPE.KEY): string {
        let signType = signTypeParam || SIGN_TYPE.SIGN_TYPE_MD5;
        if (signType !== SIGN_TYPE.SIGN_TYPE_MD5 && signType !== SIGN_TYPE.SIGN_TYPE_HMACSHA256) {
            throw new Error('Invalid signType: ' + signType)
        }
        let combineStr = '';
        let sortArray = Object.keys(data).sort();
        for (let i = 0; i < sortArray.length; ++i) {
            let key = sortArray[i];
            if (key !== this.FIELD_SIGN && data[key]) {
                let value = String(data[key]);
                if (value.length > 0) {
                    combineStr = combineStr + key + '=' + value + '&'
                }
            }
        }
        if (combineStr.length === 0) {
            throw new Error('There is no data to generate signature')
        } else {
            if (signKeyType === SIGN_KEY_TYPE.KEY) {
                combineStr = combineStr + 'key=' + key
            } else {
                if (signType !== SIGN_TYPE.SIGN_TYPE_MD5) {
                    throw new Error('work wx generate signature require the use of md5')
                }
                combineStr = combineStr + 'secret=' + key
            }

            if (signType === SIGN_TYPE.SIGN_TYPE_MD5) {
                return this.md5(combineStr).toUpperCase();
            } else if (signType === SIGN_TYPE.SIGN_TYPE_HMACSHA256) {
                return this.hmacsha256(combineStr, key).toUpperCase();
            } else {
                throw new Error('Invalid signType: ' + signType)
            }
        }
    }

    /**
     * 拼接参数
     * @param map     待拼接的 Map 数据
     * @param connStr 连接符
     * @param encode  是否 urlencode
     * @param quotes  是否 ""
     */
    public static createLinkString(map: Map<string, string>, connStr: string, encode: boolean, quotes: boolean): string {
        // 排序
        let arrayObj = Array.from(map);
        arrayObj.sort((a: any, b: any) => {
            return a[0].localeCompare(b[0])
        });
        let content: string = '';
        for (let i = 0; i < arrayObj.length; i++) {
            let key = arrayObj[i][0];
            let value = arrayObj[i][1];
            // 拼接时，不包括最后一个&字符
            if (i == arrayObj.length - 1) {
                if (quotes) {
                    content = content.concat(key).concat('=').concat('"').concat(encode ? urlencode.encode(value) : value).concat('"')
                } else {
                    content = content.concat(key).concat('=').concat(encode ? urlencode.encode(value) : value)
                }
            } else {
                if (quotes) {
                    content = content.concat(key).concat('=').concat('"').concat(encode ? urlencode.encode(value) : value).concat('"').concat(connStr)
                } else {
                    content = content.concat(key).concat('=').concat(encode ? urlencode.encode(value) : value).concat(connStr)
                }
            }
        }
        return content
    }

    /**
     * 构建请求签名参数
     * @param method  Http 请求方式
     * @param url 请求接口 /v3/certificates
     * @param timestamp 获取发起请求时的系统当前时间戳
     * @param nonceStr 随机字符串
     * @param body 请求报文主体
     */
    public static buildReqSignMessage(method: string, url: string, timestamp: string, nonceStr: string, body: string): string {
        return method.concat('\n').concat(url).concat('\n').concat(timestamp).concat('\n').concat(nonceStr).concat('\n').concat(body).concat('\n')
    }

    /**
     * 获取授权认证信息
     * @param mchId     商户号
     * @param serialNo  商户API证书序列号
     * @param nonceStr  请求随机串
     * @param timestamp 时间戳
     * @param signature 签名值
     * @param authType  认证类型，目前为WECHATPAY2-SHA256-RSA2048
     */
    public static getAuthorization(mchId: string, serialNo: string, nonceStr: string, timestamp: string, signature: string, authType: string): string {
        let map: Map<string, string> = new Map();
        map.set('mchid', mchId);
        map.set('nonce_str', nonceStr);
        map.set('signature', signature);
        map.set('timestamp', timestamp);
        map.set('serial_no', serialNo);
        return authType.concat(' ').concat(this.createLinkString(map, ',', false, true))
    }

    /**
     * 构建 v3 接口所需的 Authorization
     * @param method     请求方法
     * @param urlSuffix  APi名称,
     * @param mchId     商户Id
     * @param serialNo  商户 API 证书序列号
     * @param key       key.pem 证书API密钥
     * @param body      接口请求参数
     */
    public static async buildAuthorization(method: string, urlSuffix: string, mchId: string, serialNo: string, key: string, body: string): Promise<string> {
        let timestamp: string = parseInt((Date.now() / 1000).toString()).toString(); // 时间戳
        let authType: string = 'WECHATPAY2-SHA256-RSA2048'; // 认证类型
        let nonceStr = 'payscore' + intformat(flakeIdGen.next(), 'dec'); // 随机字符串
        // 构建签名参数
        let buildSignMessage: string = this.buildReqSignMessage(method, urlSuffix, timestamp, nonceStr, body);
        // 生成签名
        let buffer = Buffer.from(key)
        let signature: string = this.sha256WithRsa(buffer, buildSignMessage);
        // 根据平台规则生成请求头 authorization
        return this.getAuthorization(mchId, serialNo, nonceStr, timestamp, signature, authType)
    }

    /**
     * 获取请求头
     * @param authorization 授权信息
     * @param serialNumber  证书序列号
     */
    public static getHeaders(authorization: string, serialNumber: string): Object {
        let userAgent: string = 'WeChatPay-Score-HttpClient/%s (%s) nodejs/%s';
        userAgent = util.format(
            userAgent,
            '2.4.0',
            os
                .platform()
                .concat('/')
                .concat(os.release()),
            process.version
        );
        return {
            Authorization: authorization,
            Accept: 'application/json',
            'Content-type': 'application/json',
            'Wechatpay-Serial': serialNumber,
            'User-Agent': userAgent
        }
    }

    /**
     *
     * @param {string} fmt
     * @returns {string}
     * @constructor
     */
    public static Format(fmt: string): any {
        let date = new Date();
        let o: any = {
            "M+": date.getMonth() + 1,                 //月份
            "d+": date.getDate(),                    //日
            "h+": date.getHours(),                   //小时
            "m+": date.getMinutes(),                 //分
            "s+": date.getSeconds(),                 //秒
            "q+": Math.floor((date.getMonth() + 3) / 3), //季度
            "S": date.getMilliseconds()             //毫秒
        };
        if (/(y+)/.test(fmt))
            fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (let k in o)
            if (new RegExp("(" + k + ")").test(fmt)) {
                fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
            }
        return fmt;
    }
}
